"""Contains various data types used throughout Pyre"""

from __future__ import division

import math

class Coordinate(object):
    """Represents a 2D integer Cartesian coordinate"""

    def __init__(self, x, y):
        self._val = x, y

    @property
    def x(self):
        """
        Coordinate.x: int
        Gets the x value of the Coordinate
        """

        return self[0]

    @property
    def y(self):
        """
        Coordinate.y: int
        Gets the y value of the Coordinate
        """

        return self[1]

    @classmethod
    def copy(cls, other):
        """
        Coordinate.copy(Coordinate): Coordinate
        Creates a copy of the given Coordinate
        """

        return cls(other[0], other[1])

    def set_xy(self, x, y):
        """
        Coordinate.set_xy(int, int): None
        Updates the value of the Coordinate
        """

        self._val = x, y

    def __getitem__(self, index):
        return self._val[index]

    def __len__(self):
        return len(self._val)

    def __iter__(self):
        return iter(self._val)

    def __hash__(self):
        return hash(self._val)

    def __eq__(self, other):
        return self[0] == other[0] and self[1] == other[1] and len(other) == 2

    def __ne__(self, other):
        return self[0] != other[0] or self[1] != other[1] or len(other) != 2

    def __add__(self, other):
        return Coordinate(self[0] + other[0], self[1] + other[1])

    def __radd__(self, other):
        return Coordinate(other[0] + self[0], other[1] + self[1])

    def __iadd__(self, other):
        self._val = self[0] + other[0], self[1] + other[1]
        return self

    def __sub__(self, other):
        return Coordinate(self[0] - other[0], self[1] - other[1])

    def __rsub__(self, other):
        return Coordinate(other[0] - self[0], other[1] - self[1])

    def __isub__(self, other):
        self._val = self[0] - other[0], self[1] - other[1]
        return self

    def __str__(self):
        return '({}, {})'.format(self[0], self[1])

    def __repr__(self):
        return 'Coordinate({}, {})'.format(self[0], self[1])

    def direction_to(self, other):
        """
        Coordinate.direction_to(Coordinate): Coordinate
        Returns the cardinal direction represented as a Coordinate which will
        get from this Coordinate to the given one
        """

        return Coordinate(max(-1, min(other[0] - self[0], 1)),
                          max(-1, min(other[1] - self[1], 1)))

    def directions_to(self, other):
        """
        Coordinate.directions_to(Coordinate): Coordinate
        Returns the cardinal directions represented as a Coordinates which will
        get from this Coordinate to the given one
        """

        dx, dy = self.direction_to(other)
        if dx == 0 and dy == 0:
            return {Coordinate(dx, dy)}
        elif dx == 0 and dy != 0:
            return {Coordinate(dx, dy) for dx in [-1, 0, 1]}
        elif dx != 0 and dy == 0:
            return {Coordinate(dx, dy) for dy in [-1, 0, 1]}
        else:
            return {Coordinate(dx, 0), Coordinate(dx, dy), Coordinate(0, dy)}

    def distance_to(self, other):
        """
        Coordinate.distance_to(Coordinate): float
        Returns the Cartesian distance to the given Coordinate
        """

        return math.hypot(other[0] - self[0], other[1] - self[1])

    def steps_to(self, other):
        """
        Coordinate.steps_to(Coordinate): int
        Returns the Manhattan distance to the given Coordinate
        """

        return max(abs(other[0] - self[0]), abs(other[1] - self[1]))

    def is_orthogonal(self):
        """
        Coordinate.is_orthogonal(): bool
        Returns True if one and only one component of the Coordinate is zero
        """

        return (self[0] == 0) ^ (self[1] == 0)


class Direction(object):
    """A collection of Coordinate representing cardinal directions"""

    NORTH = Coordinate(0, -1)
    EAST = Coordinate(1, 0)
    SOUTH = Coordinate(0, 1)
    WEST = Coordinate(-1, 0)
    NORTHEAST = Coordinate(1, -1)
    SOUTHEAST = Coordinate(1, 1)
    SOUTHWEST = Coordinate(-1, 1)
    NORTHWEST = Coordinate(-1, -1)
    ORIGIN = Coordinate(0, 0)

    @staticmethod
    def key_to_dir(key):
        """
        Direction.key_to_dir(chr): return Coordinate
        Translates a key press into a direction represented by a Coordinate.
        """

        key = key.lower()
        delta = None

        if key == 'k' or key == '8':
            delta = Direction.NORTH
        elif key == 'l' or key == '6':
            delta = Direction.EAST
        elif key == 'j' or key == '2':
            delta = Direction.SOUTH
        elif key == 'h' or key == '4':
            delta = Direction.WEST
        elif key == 'u' or key == '9':
            delta = Direction.NORTHEAST
        elif key == 'n' or key == '3':
            delta = Direction.SOUTHEAST
        elif key == 'b' or key == '1':
            delta = Direction.SOUTHWEST
        elif key == 'y' or key == '7':
            delta = Direction.NORTHWEST
        elif key == '.' or key == '5':
            delta = Direction.ORIGIN

        return delta


class Color(object):
    """A collection of ansi escape codes for colors"""

    AQUA = 51
    BEIGE = 230
    BLACK = 16
    BLUE = 21
    BROWN = 130
    CHARTREUSE = 118
    CORAL = 209
    CRIMSON = 161
    CYAN = 51
    DARKBLUE = 18
    DARKGRAY = 145
    DARKGREEN = 22
    DARKMAGENTA = 90
    DARKORANGE = 208
    DARKRED = 88
    DARKTURQUOISE = 44
    DARKVIOLET = 92
    DARKPINK = 198
    DIMGRAY = 59
    FORESTGREEN = 28
    FUCHSIA = 201
    GOLD = 220
    GRAY = 244
    GREEN = 28
    HOTPINK = 205
    INDIGO = 54
    LAVENDER = 189
    LAWNGREEN = 82
    LIGHTBLUE = 152
    LIGHTCORAL = 210 
    LIGHTCYAN = 195
    LIGHTGREEN = 120
    LIGHTGRAY = 252
    LIGHTPINK = 217
    LIGHTSALMON = 216
    LIME = 46
    LIMEGREEN = 77
    MAGENTA = 201
    MEDIUMBLUE = 20
    MEDIUMORCHID = 134
    MIDNIGHTBLUE = 17
    OLIVE = 100
    OLIVERAB = 64
    ORANGE = 214
    ORANGERED = 202
    ORCHID = 170
    PEACHPUFF = 223
    PINK = 218
    PLUM = 182
    PURPLE = 90
    RED = 196
    SALMON = 209
    SEAGREEN = 29
    SILVER = 145
    SKYBLUE = 116
    SLATEGRAY = 66
    SMOKE = 240
    TAN = 180
    TEAL = 30
    TURQUOISE = 80
    VIOLET = 213
    WHITE = 231
    WHITESMOKE = 255
    YELLOW = 226

    @classmethod
    def by_name(cls, name):
        """
        Color.by_name(str): int
        Returns the ansi code given by name
        """

        name = name.lower()
        for attr in dir(cls):
            if attr.lower() == name:
                return getattr(cls, attr)

    @classmethod
    def try_by_name(cls, name):
        """
        Color.try_by_name(str): int
        Tries to return the ansi code given by name. If the name isn't found,
        the name is parsed as an int and returned.
        """

        color = cls.by_name(name)
        if not color:
            color = int(name)
        return color

    _CUBE_RGB_VALS = [0, 95, 135, 175, 215, 255]

    @classmethod
    def to_code(cls, rgb):
        """
        Color.by_rgb(int): int
        Finds the ansi code on the 6x6x6 cube nearest the given rgb value
        """

        red = (rgb >> 16) & 0xFF
        green = (rgb >> 8) & 0xFF
        blue = rgb & 0xFF

        red = min(range(6), key=lambda x: abs(cls._CUBE_RGB_VALS[x] - red))
        green = min(range(6), key=lambda x: abs(cls._CUBE_RGB_VALS[x] - green))
        blue = min(range(6), key=lambda x: abs(cls._CUBE_RGB_VALS[x] - blue))

        return red * 36 + green * 6 + blue + 16

    _SYS_RGB  = [(0, 0, 0),
                 (0xAA, 0, 0),
                 (0, 0xAA, 0),
                 (0xAA, 0x55, 0),
                 (0, 0, 0xAA),
                 (0xAA, 0, 0xAA),
                 (0, 0xAA, 0xAA),
                 (0xAA, 0xAA, 0xAA),
                 (0x55, 0x55, 0x55),
                 (0xFF, 0x55, 0xFF),
                 (0x55, 0xFF, 0x55),
                 (0xFF, 0xFF, 0x55),
                 (0x55, 0x55, 0xFF),
                 (0xFF, 0x55, 0xFF),
                 (0x55, 0xFF, 0xFF),
                 (0xFF, 0xFF, 0xFF)]

    @classmethod
    def to_rgb(cls, code):
        assert 0 <= code < 256

        if code < 16:
            return cls._SYS_RGB[code]
        elif code < 16 + 6 ** 3:
            r, code = divmod(code - 16, 36)
            g, b = divmod(code, 6)
            return tuple(cls._CUBE_RGB_VALS[x] for x in [r, g, b])
        else:
            gray = 8 + 10 * (code - 16 - 6 ** 3)
            return gray, gray, gray


class ColoredChar(tuple):
    """Represents a character-color pair as a tuple of chr and int"""

    def __new__(cls, char, color=Color.WHITE, sprite_id=None):
        return tuple.__new__(cls, (char, color))

    def __init__(self, char, color=Color.WHITE, sprite_id=None):
        # 38;5;<color>m indicates foreground in 256 colors
        # 48;5;<color>m is for background
        self.ansi = '\033[38;5;{1}m{0}\033[0m'.format(char, color)
        self.rgba = Color.to_rgb(color)
        self.sprite_id = sprite_id

    @property
    def char(self):
        """
        ColoredChar.char: chr
        Returns the character value of the ColoredChar
        """

        return self[0]

    @property
    def color(self):
        """
        ColoredChar.color: int
        Returns the color value of the ColoredChar
        """

        return self[1]

    def recolored(self, color):
        """
        ColoredChar.recolored(int): ColoredChar
        Returns a new ColoredChar with the same character but a different color
        """

        return ColoredChar(self[0], color)

    def __repr__(self):
        return 'ColoredChar' + tuple.__repr__(self)


class DeltaClock(object):

    class Node(object):

        def __init__(self, delta, link):
            self.delta = delta
            self.link = link
            self.events = set()

    def __init__(self):
        self.head = None
        self.nodes = {}

    def schedule(self, events, delta):
        prev, curr = None, self.head
        while curr and delta > curr.delta:
            delta -= curr.delta
            prev, curr = curr, curr.link

        if curr and delta == curr.delta:
            node = curr
        else:
            node = DeltaClock.Node(delta, curr)
            if not prev:
                self.head = node
            else:
                prev.link = node
            if curr:
                curr.delta -= delta

        node.events.update(events)
        for event in events:
            assert event not in self.nodes
            self.nodes[event] = node

    def advance(self):
        events = self.head.events
        for event in events:
            del self.nodes[event]
        self.head = self.head.link
        return events

    def unschedule(self, events):
        for event in events:
            if event in self.nodes:
                self.nodes[event].events.remove(event)
                del self.nodes[event]
